#******************************************************************************
#******************************************************************************

# import libraries

import numpy as np

from dataclasses import dataclass, field, InitVar
        
#******************************************************************************
#******************************************************************************
#******************************************************************************
#******************************************************************************

@dataclass
class StandardisedPipeDatabase:
    """A class for databases of standardised pipes."""    
    pipe_tuples: list = field(init=False)
    d_int: dict = field(init=False)
    d_ext: dict = field(init=False)
    d_ins: dict = field(init=False)
    d_cas: dict = field(init=False)
    k_pipe: dict = field(init=False)
    k_ins: dict = field(init=False)
    k_cas: dict = field(init=False)
    sp: dict = field(init=False)
    length: dict = field(init=False)
    p_rated: dict = field(init=False)
    e_eff: dict = field(init=False)
    pipe_dist: dict = field(init=False) # twin pipes only
    make_ref_tuples: dict = field(init=False)
    source: InitVar[list] = None

    #**************************************************************************
    
    def read(self, filename,
             concerns_twin_pipes=False, 
             label_twinpipedist='pipe_dist'):
        
        #**********************************************************************
        
        # the CSV file should be organised as follows:
        # 1st row: labels
        # 2nd and subsequent rows: data
        # 1st column: DN (nominal diameter)
        # 2nd column: S (schedule)
        # 3rd column: nominal (internal) diameter
        # 4th column: external diameter (no insulation)
        # 5th column: external diameter with insulation
        # 6th column: external diameter with casing
        # 7th column: pipe thermal conductivity
        # 8th column: insulation thermal conductivity
        # 9th column: casing thermal conductivity
        # 10th column: specific price (price per pipe length unit)
        # 11th column: length (m)
        # 12th column: pressure rating (bar)
        # 13th column: relative pipe roughness
        # 14th column: pipe make 
        # 15th column: pipe ref
        # 16th column: distance between pipes (for double pipes)
            
        # DN,S,d_int,d_ext,d_ins,d_cas,k_pipe,k_ins,k_cas,sp,length,p_rated,e_eff,make,ref
        
        #**********************************************************************
            
        # example: P235, ISOPLUS, twin-pipe
        
        # DN,S,d_int,d_ext,d_ins,d_cas,k_pipe,k_ins,k_cas,sp,length,p_rated,e_eff,make,ref
        # 20,1,0.0249,0.0269,0.122,0.125,55,0.024,0.4,,,25,,,
        # 25,1,0.0314,0.0337,0.137,0.14,55,0.024,0.4,,,25,,,
        # 32,1,0.0398,0.0424,0.157,0.16,55,0.024,0.4,,,25,,,
        # 40,1,0.0457,0.0483,0.157,0.16,55,0.024,0.4,,,25,,,
        # 50,1,0.0574,0.0603,0.1968,0.2,55,0.024,0.4,,,25,,,
        # 65,1,0.0732,0.0761,0.2216,0.225,55,0.024,0.4,,,25,,,
        # 80,1,0.0857,0.0889,0.2464,0.25,55,0.024,0.4,,,25,,,
        # 100,1,0.1107,0.1143,0.3109,0.315,55,0.024,0.4,,,25,,,
        # 125,1,0.1361,0.1397,0.3952,0.4,55,0.024,0.4,,,25,,,
        # 150,1,0.1643,0.1683,0.4448,0.45,55,0.024,0.4,,,25,,,
        # 200,1,0.2146,0.2191,0.554,0.56,55,0.024,0.4,,,25,,,
                
        #**********************************************************************
    
        # output:
        # list of (DN,S) tuples
        # list of (make,ref) tuples
        # dict of d_int
        # dict of d_ext
        # dict of d_ins
        # dict of cas_d
        # dict of pipe_k
        # dict of ins_k
        # dict of cas_k
        # dict of sp
        # dict of cas_k
        # dict of length
        # dict of p_rated
        # dict of e_eff

        #**********************************************************************
            
        # specify or get names
            
        names = ['DN',
                 'S',
                 'd_int',
                 'd_ext',
                 'd_ins',
                 'd_cas',
                 'k_pipe',
                 'k_ins',
                 'k_cas',
                 'sp',
                 'length',
                 'p_rated',
                 'e_eff',
                 'make',
                 'ref']

        #**********************************************************************
        
        # file specifications
        
        number_header_lines = 0 # the first line is for the labels/names
        
        number_footer_lines = 0 # no lines after the data
      
        if concerns_twin_pipes:
            
            names.append(label_twinpipedist)
            
            file_dtypes = (int,         #DN
                           int,         #S
                           float,       #d_int
                           float,       #d_ext
                           float,       #d_ins
                           float,       #d_cas
                           float,       #k_pipe
                           float,       #k_ins
                           float,       #k_cas
                           float,       #sp
                           float,       #length
                           float,       #p_rated
                           float,       #e_eff
                           "|S10",     # make
                           "|S10",     # ref
                           float)       # pipe_dist         
            
        else:
        
            file_dtypes = (int,         #DN
                           int,         #S
                           float,       #d_int
                           float,       #d_ext
                           float,       #d_ins
                           float,       #d_cas
                           float,       #k_pipe
                           float,       #k_ins
                           float,       #k_cas
                           float,       #sp
                           float,       #length
                           float,       #p_rated
                           float,       #e_eff
                           "|S10",      # make
                           "|S10")      # ref
            
        # str does not work, only "|S10"

        #**********************************************************************
    
        # read the file
        
        npdata = np.genfromtxt(
            filename, 
            dtype=file_dtypes,
            names=True,
            skip_header=number_header_lines,
            skip_footer=number_footer_lines,
            delimiter=',')
        
        #**********************************************************************
                
        number_pipes = len(npdata)
        
        pipe_tuples = []
        d_int = {}
        d_ext = {}
        d_ins = {}
        d_cas = {}
        k_pipe = {}
        k_ins = {}
        k_cas = {}
        sp = {}
        length = {}
        p_rated = {}
        e_eff = {}
        make_ref_tuples = {}
        pipe_dist = {}
        
        # for each tuple
        
        for i in range(number_pipes):
            
            # get DN
            
            i_dn = npdata['DN'][i]
            
            # get S
            
            i_s = npdata['S'][i]
            
            # get make
            
            i_make = npdata['make'][i].decode()
            
            # get reference
            
            i_ref = npdata['ref'][i].decode()
            
            tuple_entry = (i_dn,i_s,i_make,i_ref)
            
            # pipe tuple
            
            pipe_tuples.append(tuple_entry)
            
            # d_int
            
            d_int.update({tuple_entry: float(npdata['d_int'][i])})
            
            # d_ext
            
            d_ext.update({tuple_entry: float(npdata['d_ext'][i])})
            
            # d_ins
            
            d_ins.update({tuple_entry: float(npdata['d_ins'][i])})
            
            # d_cas
            
            d_cas.update({tuple_entry: float(npdata['d_cas'][i])})
            
            # k_pipe
            
            k_pipe.update({tuple_entry: float(npdata['k_pipe'][i])})
            
            # k_ins
            
            k_ins.update({tuple_entry: float(npdata['k_ins'][i])})
            
            # k_cas
            
            k_cas.update({tuple_entry: float(npdata['k_cas'][i])})
            
            # sp
            
            sp.update({tuple_entry: float(npdata['sp'][i])})
            
            # length
            
            length.update({tuple_entry: float(npdata['length'][i])})
            
            # p_rated
            
            p_rated.update({tuple_entry: float(npdata['p_rated'][i])})
            
            # effective pipe roughness
            
            e_eff.update({tuple_entry: float(npdata['e_eff'][i])})
            
            # make and ref tuple
            
            make_ref_tuples.update({tuple_entry: (i_make,i_ref)})
            
            # twin pipes
            
            try:
                
                pipe_dist.update(
                    {tuple_entry: float(npdata[label_twinpipedist][i])})
                
            except ValueError:
                
                pipe_dist.update({tuple_entry: None})
                
        #**********************************************************************

        # return 
        
        return (
            pipe_tuples, 
            d_int, 
            d_ext, 
            d_ins, 
            d_cas,
            k_pipe,
            k_ins,
            k_cas,
            sp,
            length,
            p_rated,
            e_eff,
            make_ref_tuples,
            pipe_dist
            )

    #**************************************************************************

    def __post_init__(self, source):
    
        self.pipe_tuples = []
            
        self.d_int = {}
            
        self.d_ext = {}
            
        self.d_ins = {}
            
        self.d_cas = {}
            
        self.k_pipe = {}
            
        self.k_ins = {}
            
        self.k_cas = {}
        
        self.sp = {}
            
        self.length = {}
            
        self.p_rated = {}
            
        self.e_eff = {}
        
        self.make_ref_tuples = {}
        
        self.pipe_dist = {}
        
        label_twinpipedist = 'pipe_dist'
        
        for iteration, file in enumerate(source):
            
            # determine if the file headers include the label for pipe spacing
            
            f = open(file, "r", encoding="utf-8")
            
            if label_twinpipedist in f.readline():
                
                concerns_twin_pipes = True
                
            else:
                
                concerns_twin_pipes = False
                
            f.close()
            
            # read the data
            
            (tmp_pipe_tuples, 
             tmp_d_int, 
             tmp_d_ext, 
             tmp_d_ins, 
             tmp_d_cas, 
             tmp_k_pipe, 
             tmp_k_ins, 
             tmp_k_cas, 
             tmp_sp, 
             tmp_length, 
             tmp_p_rated,
             temp_e_eff,
             temp_make_ref_tuples,
             temp_pipe_dist) = self.read(
                 file,
                 concerns_twin_pipes=concerns_twin_pipes,
                 label_twinpipedist=label_twinpipedist)
            
            if iteration == 0:
                
                # dicts are empty
            
                self.pipe_tuples = tmp_pipe_tuples
                    
                self.d_int = tmp_d_int
                    
                self.d_ext = tmp_d_ext
                    
                self.d_ins = tmp_d_ins
                    
                self.d_cas = tmp_d_cas
                    
                self.k_pipe = tmp_k_pipe
                    
                self.k_ins = tmp_k_ins
                    
                self.k_cas = tmp_k_cas
                
                self.sp = tmp_sp
                    
                self.length = tmp_length
                    
                self.p_rated = tmp_p_rated
                    
                self.e_eff = temp_e_eff
            
                self.make_ref_tuples = temp_make_ref_tuples
                
                # twin pipes
                
                self.pipe_dist = temp_pipe_dist
                
            else:
                
                # check for redundancies
                
                # if none, update
                
                # if some, exclude redundant terms
                
                self.pipe_tuples.extend(tmp_pipe_tuples)
                    
                self.d_int.update(tmp_d_int)
                    
                self.d_ext.update(tmp_d_ext)
                    
                self.d_ins.update(tmp_d_ins)
                    
                self.d_cas.update(tmp_d_cas)
                    
                self.k_pipe.update(tmp_k_pipe)
                    
                self.k_ins.update(tmp_k_ins)
                    
                self.k_cas.update(tmp_k_cas)
                
                self.sp.update(tmp_sp)
                    
                self.length.update(tmp_length)
                    
                self.p_rated.update(tmp_p_rated)
                    
                self.e_eff.update(temp_e_eff)
                    
                self.make_ref_tuples.update(temp_make_ref_tuples)
                
                # twin pipes
                
                self.pipe_dist.update(temp_pipe_dist)
                    
            #******************************************************************
                
        #**********************************************************************

    #**************************************************************************
    
#******************************************************************************
#******************************************************************************
#******************************************************************************
#******************************************************************************
#******************************************************************************

@dataclass
class Pipe:
    """A class for cylindrical pipes."""
    length: float = None
    k: float = None
    e_eff: float = None # field(init=False)
    d_int: float = None # field(init=False)
    d_ext: float = None # field(init=False)
    
    def CrossSectionalArea(self, diameter):
        
        return np.pi*(diameter/2)**2
        
    def InternalCrossSectionalArea(self):
        
        return self.CrossSectionalArea(self.d_int)
    
    def SurfaceArea(self, diameter, length):
        
        return np.pi*diameter*length
    
    def InternalSurfaceArea(self):
        
        return self.SurfaceArea(self.d_int, self.length)
    
    def ValidatePipe(self):
        
        # the pipe length must be positive
        
        assert self.length > 0
        
        # the thermal conductivity must be positive
        
        assert self.k > 0
        
        # the absolute pipe roughness must be non-negative
        
        assert self.e_eff >= 0
        
        # the internal diameter must be positive
        
        assert self.d_int > 0
        
        # the external diameter must be positive
        
        assert self.d_ext > 0
        
        # the external diameter must be greater than or equal to the int. dia.
        
        assert self.d_ext >= self.d_int

@dataclass
class Pipe:
    """A class for cylindrical pipes."""
    length: float = None
    k: float = None
    e_eff: float = None # field(init=False)
    d_int: float = None # field(init=False)
    d_ext: float = None # field(init=False)
    
    def CrossSectionalArea(self, diameter):
        
        return np.pi*(diameter/2)**2
        
    def InternalCrossSectionalArea(self):
        
        return self.CrossSectionalArea(self.d_int)
    
    def SurfaceArea(self, diameter, length):
        
        return np.pi*diameter*length
    
    def InternalSurfaceArea(self):
        
        return self.SurfaceArea(self.d_int, self.length)
    
    def ValidatePipe(self):
        
        # the pipe length must be positive
        
        assert self.length > 0
        
        # the thermal conductivity must be positive
        
        assert self.k > 0
        
        # the absolute pipe roughness must be non-negative
        
        assert self.e_eff >= 0
        
        # the internal diameter must be positive
        
        assert self.d_int > 0
        
        # the external diameter must be positive
        
        assert self.d_ext > 0
        
        # the external diameter must be greater than or equal to the int. dia.
        
        assert self.d_ext >= self.d_int
    
#******************************************************************************
#******************************************************************************
#******************************************************************************
#******************************************************************************
#******************************************************************************

@dataclass
class InsulatedPipe(Pipe):
    """A class for insulated cylindrical pipes."""
    d_ins: float = None
    d_cas: float = None
    k_ins: float = None
    k_cas: float = None
    
    def ValidateInsulatedPipe(self):
        
        # validate inherited attributes
        
        self.ValidatePipe()
        
        # the insulation's thermal conductivity must be positive
        
        assert self.k_ins > 0
        
        # the casing's thermal conductivity must be positive
        
        assert self.k_cas > 0
        
        # the external diameter must be positive
        
        assert self.d_ins > 0
        
        # the insulation diameter must be positive
        
        assert self.d_cas > 0
        
        # the external diameter must be greater than or equal to the ins. dia.
        
        assert self.d_cas >= self.d_ins
        
        # the insulation diameter must be greater than or equal to the ext. di.
        
        assert self.d_ins >= self.d_ext
    
#******************************************************************************
#******************************************************************************
#******************************************************************************
#******************************************************************************
#******************************************************************************

@dataclass
class StandardisedPipe(InsulatedPipe):
    """A class for standardised cylindrical pipes."""
    pipe_tuple: tuple = None          # (DN,S,make,reference)
    e_rel: float = None             # relative roughness
    sp: float = None                # specific price
    p_rated: float = None           # rated pressure
    #make_ref: tuple = None          # make and ref
    db: InitVar[StandardisedPipeDatabase] = None
    
    #**************************************************************************
    #**************************************************************************
    
    def __post_init__(self, db):
        
        # class instances can be created using a database or manually
        
        if db != None:
            
            # a database has been specified
            
            # check if the database has information about the pipe
        
            assert self.pipe_tuple in db.pipe_tuples
                
            # get data from the database
            
            self.d_int = db.d_int[self.pipe_tuple]
            
            self.d_ext = db.d_ext[self.pipe_tuple]
            
            self.d_ins = db.d_ins[self.pipe_tuple]
            
            self.d_cas = db.d_cas[self.pipe_tuple]
            
            self.k = db.k_pipe[self.pipe_tuple]
            
            self.k_ins = db.k_ins[self.pipe_tuple]
            
            self.k_cas = db.k_cas[self.pipe_tuple]
        
            self.p_rated = db.p_rated[self.pipe_tuple]
            
            # if sp has not been defined manually...
            
            if self.sp == None:
                
                # ...get it from the data base
            
                self.sp = db.sp[self.pipe_tuple]
                
            # if length has not been defined manually...
            
            if self.length == None:
                
                # ...get it from the data base
            
                self.length = db.length[self.pipe_tuple]
                
            # there are 3 ways to define the pipe roughness:
            # 1) by defining directly
            # 2) by defining indirectly via the relative pipe roughness
            # 3) via a database (neither one or the other)
    
            # if e_rel has not been defined manually...
            
            if (self.e_rel == None and self.e_eff == None):
                
                # ... get e_eff from the database and...
            
                self.e_eff = db.e_eff[self.pipe_tuple]
            
                # ... compute e_rel with it
                
                self.e_rel = self.e_eff/self.d_int
                
            elif (self.e_rel == None and self.e_eff != None):
                
                # direct method
                
                self.e_rel = self.e_eff/self.d_int
            
            else: 
                
                # indirect method
                
                self.e_eff = self.d_int*self.e_rel
                
            #******************************************************************
                
        else:
            
            # no database has been specified, i.e., manual mode
            
            if self.e_rel != None and self.e_eff == None:
                
                # the absolute roughness has not been specified
                
                self.e_eff = self.e_rel*self.d_int
                
            elif self.e_rel == None and self.e_eff != None:
                
                # the relative roughness has not been specified
                
                self.e_rel = self.e_eff/self.d_int
            
        #**********************************************************************
        #**********************************************************************
        
    #**************************************************************************
    #**************************************************************************
    
    def ValidateStandardisedPipe(self):
        
        # validate inherited attributes
        
        self.ValidateInsulatedPipe()
    
        # the relative roughness must be non-negative
        
        assert self.e_rel >= 0
        
        # the price must be non-negative
        
        assert self.sp >= 0
        
        # the rated pressure must be positive
        
        assert self.p_rated > 0
        
        # the input tuple must be in the database or be None
        
        assert (
            (self.pipe_tuple in self.db.pipe_tuples 
             if self.db != None else True) or
            (self.pipe_tuple == None)
            )
        
    #**************************************************************************
    #**************************************************************************
    
    # pipe_tuple: tuple = None          # (DN,S,make,reference)
    # e_rel: float = None             # relative roughness
    # sp: float = None                # specific price
    # p_rated: float = None           # rated pressure
    # make_ref: tuple = None          # make and ref
    # db: InitVar[StandardisedPipeDatabase] = None
    
#******************************************************************************
#******************************************************************************
#******************************************************************************
#******************************************************************************
#******************************************************************************